/**
 * 
 */
package engine;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import common.IObserver;
import common.tuple.Tuple5;

import data.Data;
import data.Data.Direction;
import data.Data.PlayerState;
import data.Data.Race;
import data.Data.Role;
import data.Data.TeamID;
import data.Player;
import data.Position;
import data.Team;
import exception.BadPositionException;
import exception.IllegalPlacementException;

/*
 * Reference image (not actual implementation - see below):
 *  (0,0)                            (0,14)
 *    +--------------------------------+
 *  0 |           End zone A           |
 *    +--------+--------------+--------+
 *  1 |        |              |        |
 *  2 |        |              |        |
 *  3 |        |              |    R   |
 *  4 |   L    |              |    I   |
 *  5 |   E    |              |    G   |
 *  6 |   F    |              |    H   |
 *  7 |   T    |              |    T   |
 *  8 |        |              |        |
 *  9 |   W    |              |    W   |
 * 10 |   I    |              |    I   |
 * 11 |   D    |              |    D   |
 * 12 |   E    |  SCRIMAGE A  |    E   | (12, 14)
 *    +--------+--------------+--------+ 
 * 13 |   Z    |  SCRIMAGE B  |    Z   |
 * 14 |   O    |              |    O   |
 * 15 |   N    |              |    N   |
 * 16 |   E    |              |    E   |
 * 17 |        |              |        |
 * 18 |   L    |              |    R   |
 * 19 |   E    |              |    I   |
 * 20 |   F    |              |    G   |
 * 21 |   T    |              |    H   |
 * 22 |        |              |    T   |
 * 23 |        |              |        |
 * 24 |        |              |        |
 *    +--------------------------------+
 * 25 |           End zone B           |
 *    +--------+--------------+--------+
 *     0 1 2 3  4 5 6 7 8 9 10 11121314
 * (25,0)                           (25,14)
 * 
 * 
 * 
 * Actual implementation: 
 * 
 *     0   1  2  3  4  5  6  7  8  9  10 11 12  13 14 15 16 17 18 19 20 21 22 23 24  25
 *    +---+------------------------------------+------------------------------------+---+
 *  0 |   |                                    |                                    |   |
 *  1 |   |                                    |                                    |   |
 *  2 |   |                                    |                                    |   |
 *  3 | E |                                    |                                    | E |
 *    + N +------------------------------------+------------------------------------+ N +
 *  4 | D |                                  S | S                                  | D |
 *  5 |   |                                  C | C                                  |   |
 *  6 | Z |                                  R | R                                  | Z |
 *  7 | O |                                  I | I                                  | O |
 *  8 | N |                                  M | M                                  | N |
 *  9 | E |                                    |                                    | E |
 * 10 |   |                                  A | B                                  |   |
 *    +   +------------------------------------+------------------------------------+   + 
 * 11 | A |                                    |                                    | B |
 * 12 |   |                                    |                                    |   |
 * 13 |   |                                    |                                    |   |
 * 14 |   |                                    |                                    |   |
 *    +---+------------------------------------+------------------------------------+---+
 * 
 * 
 * 
 * 
 * 
 * 
 */

/**
 * Represents the playing field in BloodBowl.
 * 
 * @author bogdan
 * 
 */
public class Pitch implements Serializable {
	private static final long serialVersionUID = -5298695017255042307L;

	// pitch dimensions
	public final static int PITCH_WIDTH = 26;
	public final static int PITCH_HEIGHT = 15;
	public final static int MAX_PLAYERS_ON_FIELD = 11; // TODO: make sure this
														// number is correct

	/**
	 * Helper class. Maintains the actual map structure and performs the
	 * necessary changes to it.
	 * 
	 */
	public class MapContainer implements Serializable {
		private static final long serialVersionUID = 8927828179059856340L;

		// these are references to the team objects in GameEngine.
		// they should not be changed in this class,
		// but are useful for getting players from the field by location
		public final Team TeamA;
		public final Team TeamB;

		private final int[][] m_map;

		// the unique code of the ball on the map
		public static final int BALL_CODE = -1;
		public static final int EMPTY_CODE = 0;

		/**
		 * Ball position.
		 */
		private Position ballPosition;

		/**
		 * Constructor.
		 * 
		 * @param teamA
		 *            - reference to the TeamA team
		 * @param teamB
		 *            - reference to the TeamB team
		 */
		public MapContainer(final Team teamA, final Team teamB) {
			TeamA = teamA;
			TeamB = teamB;

			m_map = new int[PITCH_WIDTH][PITCH_HEIGHT];

			for (int i = 0; i < m_map.length; ++i) {
				for (int j = 0; j < m_map[i].length; ++j) {
					m_map[i][j] = 0;
				}
			}
		}

		private MapContainer(final MapContainer map) {
			TeamA = map.TeamA;
			TeamB = map.TeamB;

			m_map = new int[PITCH_WIDTH][PITCH_HEIGHT];

			for (int i = 0; i < m_map.length; ++i) {
				for (int j = 0; j < m_map[i].length; ++j) {
					m_map[i][j] = map.m_map[i][j];
				}
			}
		}

		/**
		 * Gets a read-only reference to the map.
		 * 
		 * @return Returns a read only reference to the internal map
		 *         representation.
		 */
		public final int[][] getMap() {
			return m_map;
		}

		/**
		 * Encodes a player by an integer.
		 * 
		 * The encoding format is as follows: "tRrnnb" where t = team R = Race r
		 * = role nn = number b = player has the ball
		 * 
		 * @param team
		 *            - the team to which the player belongs
		 * @param player
		 *            - the player object
		 * @return Returns an integer representing the unique code of the player
		 *         on the map
		 */
		private int generatePlayerCode(final TeamID team, final Player player) {
			final int hasBall = player.hasBall() ? 1 : 0;
			return ((team.value() * 100 + player.race().value() * 10 + player.role().value()) * 100 + player.number()) * 10 + hasBall;
		}

		/**
		 * Decodes a code from the map structure.
		 * 
		 * @param code
		 *            - the code to decode
		 * @return Returns the decoded information as elements in a tuple.
		 */
		private Tuple5<TeamID, Race, Role, Integer, Boolean> decodePlayerCode(int code) {
			if (code == EMPTY_CODE) {
				return new Tuple5<TeamID, Race, Role, Integer, Boolean>();
			}
			if (code == BALL_CODE) {
				return new Tuple5<TeamID, Race, Role, Integer, Boolean>(null, null, null, null, true);
			}

			Data.Race race = Race.UNKNOWN;
			Data.Role role = Role.UNKNOWN;
			int number;
			// PlayerState state;

			// state = code % 10 == 1 ? PlayerState.HAS_BALL :
			// PlayerState.PLACED;
			code = code / 10;

			number = code % 100;
			code = code / 100;

			int r = code % 10;
			if (r == Role.BLITZER.value()) {
				role = Role.BLITZER;
			}
			if (r == Role.BLOCKER.value()) {
				role = Role.BLOCKER;
			}
			if (r == Role.CATCHER.value()) {
				role = Role.CATCHER;
			}
			if (r == Role.LINEMAN.value()) {
				role = Role.LINEMAN;
			}
			if (r == Role.THROWER.value()) {
				role = Role.THROWER;
			}
			code = code / 10;

			r = code % 10;
			if (r == Race.HUMAN.value()) {
				race = Race.HUMAN;
			}
			if (r == Race.ORC.value()) {
				race = Race.ORC;
			}
			if (r == Race.ELF.value()) {
				race = Race.ELF;
			}
			code = code / 10;

			final TeamID team = code % 10 == TeamID.TEAM_A.value() ? TeamID.TEAM_A : TeamID.TEAM_B;

			return new Tuple5<TeamID, Race, Role, Integer, Boolean>(team, race, role, number, false);
		}

		/**
		 * Places the ball on the field.
		 * 
		 * @param pos
		 *            - the position of the ball
		 * @throws BadPositionException
		 */
		public void placeBall(final Position pos) throws BadPositionException {
			if (pos.x() < 0 || pos.x() > PITCH_WIDTH || pos.y() < 0 || pos.y() > PITCH_HEIGHT) {
				throw new BadPositionException(pos);
			}

			m_map[pos.x()][pos.y()] = BALL_CODE;
			ballPosition = pos;
		}

		/**
		 * Getter for the "ballPosition" property.
		 * 
		 * @return the ballPosition
		 * 
		 * @author Tomasz Dąbrowski
		 */
		public Position getBallPosition() {
			return ballPosition;
		}

		/**
		 * Physically places the player on the map.
		 * 
		 * Takes care of maintaining the internal position in the Player object.
		 * 
		 * @param team
		 *            - the player's team
		 * @param player
		 *            - the player to place on the map
		 * @param pos
		 *            - the position of the player
		 * @throws IllegalPlacementException
		 *             , BadPositionException
		 */
		public void placePlayer(final TeamID team, final Player player, final Position pos) throws IllegalPlacementException,
				BadPositionException {
			if (pos.x() < 0 || pos.x() > PITCH_WIDTH || pos.y() < 0 || pos.y() > PITCH_HEIGHT) {
				throw new BadPositionException(pos);
			}

			if (m_map[pos.x()][pos.y()] != EMPTY_CODE) {
				throw new IllegalPlacementException("Position is occupied");
			}

			final int code = generatePlayerCode(team, player);
			m_map[pos.x()][pos.y()] = code;

			// make sure the player has the new position, after it has been
			// placed
			// (in case an exception is thrown before)
			player.setPosition(pos);
		}

		/**
		 * Moves the player on the map. Changes the player state if the player
		 * picks up the ball.
		 * 
		 * @param player
		 *            - the player to move
		 * @param newPos
		 *            - the new position of the player
		 * @throws IllegalPlacementException
		 *             , BadPositionException
		 */
		public void movePlayer(final Player player, final Position newPos) throws IllegalPlacementException, BadPositionException {
			if (newPos.x() < 0 || newPos.x() > PITCH_WIDTH || newPos.y() < 0 || newPos.y() > PITCH_HEIGHT) {
				throw new BadPositionException(newPos);
			}

			if (m_map[newPos.x()][newPos.y()] != EMPTY_CODE && m_map[newPos.x()][newPos.y()] != BALL_CODE) {
				throw new IllegalPlacementException("Position is occupied");
			}

			// pick up the ball if it's on the ground
			if (m_map[newPos.x()][newPos.y()] == BALL_CODE) {
				player.setHasBall(true);
			}

			// copy the original code to the new position first
			m_map[newPos.x()][newPos.y()] = m_map[player.x()][player.y()];
			if (player.hasBall()) {
				ballPosition = newPos;
			}
			// remove the player from the old position
			m_map[player.x()][player.y()] = EMPTY_CODE;
			// make sure the player position info is accurate
			player.setPosition(newPos);
		}

		/**
		 * Removes a player from the field.
		 * 
		 * @param team
		 *            - the team of the player.
		 * @param player
		 *            - the player object.
		 */
		public void removePlayer(final TeamID team, final Player player) {
			if (player.hasBall()) {
				// the player drops the ball when is removed
				m_map[player.x()][player.y()] = BALL_CODE;
				ballPosition = new Position(player.x(), player.y());
			} else {
				m_map[player.x()][player.y()] = 0;
			}
		}

		/**
		 * Removes all the players from the field.
		 */
		public void clearField() {
			for (int i = 0; i < m_map.length; ++i) {
				for (int j = 0; j < m_map[i].length; ++j) {
					if (m_map[i][j] != EMPTY_CODE) {
						if (m_map[i][j] == BALL_CODE) {
							m_map[i][j] = EMPTY_CODE;
							ballPosition = null;
						} else {
							final Player p = getPlayer(m_map[i][j]);
							p.setState(PlayerState.NONE);
							p.setHasBall(false);
							m_map[i][j] = EMPTY_CODE;
						}
					}
				}
			}
		}

		/**
		 * Checks if a location on the map is empty or not.
		 * 
		 * @param pos
		 *            - the position to check.
		 * @return Returns true if the position is empty, false otherwise.
		 */
		public boolean isLocationEmpty(final Position pos) {
			if (isInsidePitch(pos)) {
				return m_map[pos.x()][pos.y()] == EMPTY_CODE;
			} else {
				return false;
			}
		}

		/**
		 * Gets the player located at a certain position on the map.
		 * 
		 * @param pos
		 *            - the position to get the player from.
		 * @return Returns a player object if found on the map, null otherwise.
		 */
		public Player getPlayer(final Position pos) {
			return getPlayer(decodePlayerCode(m_map[pos.x()][pos.y()]));
		}

		/**
		 * Gets the player by code.
		 * 
		 * @param code
		 *            - the code representation of the player on the map.
		 * @return Returns a reference to the requested player object. If the
		 *         code cannot be parsed, returns null.
		 */
		public Player getPlayer(final int code) {
			return getPlayer(decodePlayerCode(code));
		}

		private Player getPlayer(final Tuple5<TeamID, Race, Role, Integer, Boolean> tuple) {
			if (!tuple.isEmpty() && !tuple.getObject5()) {
				switch (tuple.getObject1()) {
				case TEAM_A:
					for (final Player p : TeamA.players()) {
						if (p.race().equals(tuple.getObject2()) && p.role().equals(tuple.getObject3())
								&& tuple.getObject4().equals(p.number())) {
							return p;
						}
					}
					break;
				case TEAM_B:
					for (final Player p : TeamB.players()) {
						if (p.race().equals(tuple.getObject2()) && p.role().equals(tuple.getObject3())
								&& tuple.getObject4().equals(p.number())) {
							return p;
						}
					}
					break;
				}
			}

			return null;
		}

		/**
		 * Checks if the ball is at the specified position
		 * 
		 * @param pos
		 *            - the given position
		 * @return Returns true if the ball is at the specified position, false
		 *         otherwise
		 */
		public boolean isBallAtPosition(final Position pos) {
			if (isInsidePitch(pos)) {
				return m_map[pos.x()][pos.y()] == BALL_CODE;
			} else {
				return false;
			}
		}

		@Override
		public String toString() {
			// the map will be translated for better display
			final int[][] map = new int[PITCH_HEIGHT][PITCH_WIDTH];
			for (int i = 0; i < PITCH_HEIGHT; ++i) {
				for (int j = 0; j < PITCH_WIDTH; ++j) {
					map[i][j] = m_map[j][i];
				}
			}

			String s = "    0   1  2  3  4  5  6  7  8  9  10 11 12  13 14 15 16 17 18 19 20 21 22 23 24  25\n";
			s += "   +---+------------------------------------+------------------------------------+---+\n";
			for (int i = 0; i < map.length; ++i) {

				if (i >= 10) {
					s += i + " ";
				} else {
					s += i + "  ";
				}
				s += "|";

				for (int j = 0; j < map[i].length; ++j) {
					if (map[i][j] == EMPTY_CODE) {
						s += "   ";
					} else if (map[i][j] == BALL_CODE) {
						s += "b  ";
					} else {
						final Tuple5<TeamID, Race, Role, Integer, Boolean> p = decodePlayerCode(map[i][j]);

						if (p.getObject5() == true) {
							s += ".b.";
						} else {
							switch (p.getObject2()) {
							case HUMAN:
								s += "h" + p.getObject1().value();
								break;
							case ORC:
								s += "o" + p.getObject1().value();
								break;
							case ELF:
								s += "e" + p.getObject1().value();
								break;
							default:
								break;
							}
							if (getPlayer(p).hasBall()) {
								s += "b";
							} else {
								s += " ";
							}
						}

					}

					if (j == map[i].length / 2 - 1 || j == 0 || j == map[i].length - 2) {
						s += "|";
					}
				}
				s += "|\n";
			}

			s += "   +---+------------------------------------+------------------------------------+---+\n";

			return s;
		}

		@Override
		public Object clone() {
			return new MapContainer(this);
		}

		/**
		 * Clears a location on the field
		 * 
		 * @param pos
		 *            - the position to clear
		 */
		public void clearCell(final Position pos) {
			if (isInsidePitch(pos)) {
				m_map[pos.x()][pos.y()] = EMPTY_CODE;
			}
		}
	}

	private MapContainer m_pitch;

	transient private ArrayList<IObserver> m_observers = new ArrayList<IObserver>();

	/**
	 * All positions.
	 */
	private static Set<Position> allPositions;

	private static HashMap<TeamID, Set<Position>> endZonePositions;;

	/**
	 * Default constructor. Creates two human teams with default names.
	 */
	public Pitch(final Team teamA, final Team teamB) {
		init(teamA, teamB);
	}

	/**
	 * Performs the necessary initialisation of the Pitch
	 * 
	 * @param teamA
	 * @param teamB
	 */
	private void init(final Team teamA, final Team teamB) {
		m_pitch = new MapContainer(teamA, teamB);
	}

	/**
	 * Registers an observer.
	 * 
	 * @param observer
	 *            - the observer to register
	 */
	public void registerObserver(final IObserver observer) {
		m_observers.add(observer);
	}

	/**
	 * Resets all observers with the current data.
	 */
	public void resetObservers() {
		notifyObservers();
	}

	/**
	 * Notifies all observers of all the changes on the pitch.
	 */
	protected void notifyObservers() {
		final MapContainer pitch = (MapContainer) m_pitch.clone();

		for (final IObserver o : m_observers) {
			o.notifyObserver(this, pitch);
		}
	}

	/**
	 * Gets a ball position.
	 * 
	 * @return ball position
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public Position getBallPosition() {
		return m_pitch.getBallPosition();
	}

	/**
	 * This class serves as a way to make specific pitch updates, instead of
	 * updating everything
	 * 
	 * @author Julien
	 * 
	 */
	public class PitchUpdateNotification {
		public MapContainer pitch;
		public Position pos;

		public PitchUpdateNotification(final MapContainer pitch, final Position pos) {
			this.pitch = pitch;
			this.pos = pos;
		}
	}

	/**
	 * Notifies all observers about changes on a specific position on the pitch.
	 * 
	 * @param pos
	 *            - the position of interest
	 */
	public void notifyObservers(final Position pos) {
		final MapContainer pitch = (MapContainer) m_pitch.clone();
		final PitchUpdateNotification payload = new PitchUpdateNotification(pitch, pos);

		for (final IObserver o : m_observers) {
			o.notifyObserver(this, payload);
		}
	}

	/**
	 * Checks if a position is in the Left Wide Zone
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in the Left Wide Zone, false
	 *         otherwise
	 */
	public static boolean isTopWideZone(final Position pos) {
		return pos.y() <= 3;
	}

	/**
	 * Checks if a position is in the Right Wide Zone
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in the Right Wide Zone, false
	 *         otherwise
	 */
	public static boolean isBottomWideZone(final Position pos) {
		return pos.y() >= 11;
	}

	/**
	 * Checks if a position is in the End Zone of Team A
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in the End Zone A, false
	 *         otherwise
	 */
	public static boolean isEndZoneA(final Position pos) {
		return pos.x() == 0;
	}

	/**
	 * Checks if a position is in the End Zone of Team B
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in the End Zone B, false
	 *         otherwise
	 */
	public static boolean isEndZoneB(final Position pos) {
		return pos.x() == 25;
	}

	/**
	 * Checks if a position is in the Scrimmage Zone
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in the Scrimmage Zone, false
	 *         otherwise
	 */
	public static boolean isScrimmageZone(final Position pos) {
		return isScrimmageZoneTeamA(pos) || isScrimmageZoneTeamB(pos);
	}

	/**
	 * Checks if a position is on the left scrimmage zone.
	 * 
	 * @param pos
	 *            the position to check
	 * @return true if one the scrimmage zone, false otherwise
	 */
	public static boolean isScrimmageZoneTeamA(final Position pos) {
		return pos.x() == 12 && pos.y() >= 4 && pos.y() <= 10;
	}

	/**
	 * Checks if a position is on the right scrimmage zone.
	 * 
	 * @param pos
	 *            the position to check
	 * @return true if one the scrimmage zone, false otherwise
	 */
	public static boolean isScrimmageZoneTeamB(final Position pos) {
		return pos.x() == 13 && pos.y() >= 4 && pos.y() <= 10;
	}

	/**
	 * Checks if a position is in Team A's half of the field
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in upper half of the filed, false
	 *         otherwise
	 */
	public static boolean isZoneTeamA(final Position pos) {
		if (pos.y() >= 0 && pos.y() < 15 && pos.x() >= 0 && pos.x() < 13) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if a position is in opposite team's half of the field.
	 * 
	 * @param pos
	 *            position to check
	 * @param team
	 *            checked team
	 * @return true if opposite team zone
	 * 
	 * @author Tomasz D�browski
	 */
	public static boolean isZoneOppositeTeam(final Position pos, final Team team) {
		if (team.getId() == TeamID.TEAM_A) {
			return isZoneTeamB(pos);
		} else {
			return isZoneTeamA(pos);
		}
	}

	/**
	 * Checks if a position is in Team B's half of the field
	 * 
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the position is in lower half of the filed, false
	 *         otherwise
	 */
	public static boolean isZoneTeamB(final Position pos) {
		if (pos.y() >= 0 && pos.y() < 15 && pos.x() >= 13 && pos.x() < 26) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if a position is inside the pitch
	 * 
	 * @param pos
	 *            The position to test
	 * @return true if the position is in the pitch, false otherwise
	 */
	public static boolean isInsidePitch(final Position pos) {
		return (pos.x() >= 0 && pos.x() < PITCH_WIDTH && pos.y() >= 0 && pos.y() < PITCH_HEIGHT);
	}

	/**
	 * Checks if a position is inside Team A's center zone.
	 * 
	 * @param pos
	 *            The position to test.
	 * @return true if the position is inside the Team A's center zone
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public static boolean isTeamACenterZone(final Position pos) {
		return pos.x() < 12 && pos.y() >= 4 && pos.y() <= 10;
	}

	/**
	 * Checks if a position is inside Team B's center zone.
	 * 
	 * @param pos
	 *            The position to test.
	 * @return true if the position is inside the Team B's center zone
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public static boolean isTeamBCenterZone(final Position pos) {
		return pos.x() > 13 && pos.y() >= 4 && pos.y() <= 10;
	}

	/**
	 * Gets the player by code
	 * 
	 * @param code
	 *            - the code representation of the player on the map.
	 * @return Returns a reference to a player object.
	 */
	public Player getPlayer(final int code) {
		return m_pitch.getPlayer(code);
	}

	/**
	 * Gets the player by position.
	 * 
	 * @param pos
	 *            - the position of the player on the map.
	 * @return Returns a reference to a player object.
	 */
	public Player getPlayer(final Position pos) {
		if (!isInsidePitch(pos)) {
			return null;
		} else {
			return m_pitch.getPlayer(pos);
		}
	}

	/**
	 * Checks if the ball is at the specified position
	 * 
	 * @param pos
	 *            - the given position
	 * @return Returns true if the ball is at the given position, false
	 *         otherwise
	 */
	public boolean isBall(final Position pos) {
		return m_pitch.isBallAtPosition(pos);
	}

	/**
	 * Checks if the player can be placed at a certain position. Takes into
	 * consideration the physical availability of the position as well as any
	 * game rules that might apply.
	 * 
	 * @param teamID
	 *            - the team of the player.
	 * @param pos
	 *            - the position to check
	 * @return Returns true if the player can be placed, false otherwise
	 */
	public boolean canPlacePlayer(final TeamID teamID, final Position pos) {
		if (!m_pitch.isLocationEmpty(pos)) {
			return false;
		}

		if (teamID.equals(TeamID.TEAM_A) && isZoneTeamB(pos) || teamID.equals(TeamID.TEAM_B) && isZoneTeamA(pos)) {
			return false;
		}

		return true;
	}

	/**
	 * Places the ball on the field.
	 * 
	 * @param pos
	 *            - the position of the ball
	 * @throws BadPositionException
	 */
	public void placeBall(final Position pos) throws BadPositionException {
		m_pitch.placeBall(pos);

		notifyObservers(pos);
	}

	/**
	 * Places a player on the field.
	 * 
	 * @param teamID
	 *            - the team ID of the placed player
	 * @param player
	 *            - a reference to the player that is being placed
	 * @param pos
	 *            - the position of the player, in field coordinates.
	 * @return Returns true if the player was successfully placed, false
	 *         otherwise.
	 * @throws IllegalPlacementException
	 *             , BadPositionException
	 */
	public void placePlayer(final TeamID teamID, final Player player, final Position pos) throws IllegalPlacementException,
			BadPositionException {
		assert (player.state().equals(PlayerState.IN_DUGOUT));
		// TODO: add the layer of rules associated with player placement

		m_pitch.placePlayer(teamID, player, pos);
		player.setState(PlayerState.IN_PLAY);

		notifyObservers(pos);
	}

	/**
	 * Moves the player to the new position
	 * 
	 * @param player
	 *            - a reference to the player to be moved
	 * @param newPos
	 *            - the new position of the player
	 * @throws IllegalPlacementException
	 *             - exception thrown if the new position of the player is
	 *             invalid
	 */
	public void movePlayer(final Player player, final Position newPos) throws IllegalPlacementException, BadPositionException {
		// TODO: add the layer of rules associated with player movement
		// rules can include: number of action points the player has
		final Position oldPos = new Position(player);
		m_pitch.movePlayer(player, newPos);

		notifyObservers(oldPos);
		notifyObservers(newPos);
	}

	/**
	 * Removes a player from the field.
	 * 
	 * @param teamID
	 *            - the team to which the player belongs
	 * @param player
	 *            - the player to remove
	 */
	public void removePlayerFromField(final TeamID teamID, final Player player) {
		// TODO: implement player removal in the event of death or injury
		m_pitch.removePlayer(teamID, player);
	}

	/**
	 * Removes all players from the field
	 */
	public void clearField() {
		m_pitch.clearField();

		notifyObservers();
	}

	/**
	 * Serializes the map as a string.
	 */
	@Override
	public String toString() {
		return m_pitch.toString();
	}

	/**
	 * Gets the relatively opposite position from the source with respect to the
	 * center position.
	 * 
	 * @param center
	 *            - the fixed point
	 * @param source
	 *            - the adjacent point
	 * @return Returns the position opposite from the source with respect to the
	 *         center.
	 */
	public Position getOppositePosition(final Position center, final Position source) {
		return new Position(center.x() - source.x() + center.x(), center.y() - source.y() + center.y());
	}

	/**
	 * Checks if the given location is empty.
	 * 
	 * @param pos
	 *            - the position to check.
	 * @return Returns true if the location is empty, false otherwise.
	 */
	public boolean isEmpty(final Position pos) {
		return m_pitch.isLocationEmpty(pos);
	}

	/**
	 * Converts the direction form a position into a position on the field.
	 * 
	 * @param center
	 *            - the fixed point
	 * @param dir
	 *            - the direction offset relative to the fixed point
	 * @return Returns a position offset by one location from the center in the
	 *         given direction.
	 */
	public Position convertDirectionToPosition(final Position center, final Direction dir) {
		switch (dir) {
		case NW:
			return new Position(center.x() - 1, center.y() - 1);
		case N:
			return new Position(center.x(), center.y() - 1);
		case NE:
			return new Position(center.x() + 1, center.y() - 1);
		case W:
			return new Position(center.x() - 1, center.y());
		case E:
			return new Position(center.x() + 1, center.y());
		case SW:
			return new Position(center.x() - 1, center.y() + 1);
		case S:
			return new Position(center.x(), center.y() + 1);
		case SE:
			return new Position(center.x() + 1, center.y() + 1);
		}

		// this line is never executed
		return null;
	}

	/**
	 * Clears a position on the field
	 * 
	 * @param pos
	 *            - the position to clear
	 */
	public void clearCell(final Position pos) {
		m_pitch.clearCell(pos);
	}

	/**
	 * Gets a set of all pitch positions.
	 * 
	 * @return all positions
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public static Set<Position> getAllPositions() {
		if (allPositions != null) {
			return allPositions;
		}
		allPositions = new HashSet<Position>();
		for (int x = 0; x < Pitch.PITCH_WIDTH; x++) {
			for (int y = 0; y < Pitch.PITCH_HEIGHT; y++) {
				final Position pos = new Position(x, y);
				allPositions.add(pos);
			}
		}
		allPositions = Collections.unmodifiableSet(allPositions);
		return allPositions;
	}

	public static Set<Position> getEndZonePositions(final TeamID teamID) {
		if (endZonePositions != null) {
			return endZonePositions.get(teamID);
		}
		endZonePositions = new HashMap<TeamID, Set<Position>>();
		{
			final Set<Position> dst = new HashSet<Position>();
			for (int i = 0; i < Pitch.PITCH_HEIGHT; i++) {
				final Position position = new Position(Pitch.PITCH_WIDTH - 1, i);
				dst.add(position);
			}
			endZonePositions.put(TeamID.TEAM_A, dst);
		}
		{
			final Set<Position> dst = new HashSet<Position>();
			for (int i = 0; i < Pitch.PITCH_HEIGHT; i++) {
				final Position position = new Position(0, i);
				dst.add(position);
			}
			endZonePositions.put(TeamID.TEAM_B, dst);
		}
		return endZonePositions.get(teamID);
	}

	public static Set<Position> getPositionRange(final Collection<Position> positions, final int range) {
		final Set<Position> retPositions = new HashSet<Position>(positions.size() * 3);
		for (final Position i : positions) {
			retPositions.addAll(getPositionRange(i, range));
		}
		return retPositions;
	}

	public static Set<Position> getPositionRange(final Position position, final int range) {
		final Set<Position> positions = new HashSet<Position>();
		for (int x = -range; x <= range; x++) {
			for (int y = -range; y <= range; y++) {
				final Position pos = new Position(position.x() + x, position.y() + y);
				if (!isInsidePitch(pos)) {
					continue;
				}
				positions.add(pos);
			}
		}
		return positions;
	}
}
